%%%-------------------------------------------------------------------
%%% Created : 30 wrzesień 2007
%%%-------------------------------------------------------------------
-module(scanner).
%-compile([native]).
-export([
         test/0,
	 scan/1
        ]).

checkIdentifier("I") -> {keyword, kw_i};
checkIdentifier("HAS") -> {keyword, kw_has};
checkIdentifier("A") -> {keyword, kw_a};
checkIdentifier("ITZ") -> {keyword, kw_itz};
checkIdentifier("R") -> {keyword, kw_r};
checkIdentifier("HAI") -> {keyword, kw_hai};
checkIdentifier("KTHXBYE") -> {keyword, kw_kthxbye};

checkIdentifier("OF") -> {keyword, kw_of};
checkIdentifier("SUM") -> {binary_math_op, kw_sum};
checkIdentifier("DIFF") -> {binary_math_op, kw_diff};
checkIdentifier("PRODUKT") -> {binary_math_op, kw_produkt};
checkIdentifier("QUOSHUNT") -> {binary_math_op, kw_quoshunt};
checkIdentifier("MOD") -> {binary_math_op, kw_mod};
checkIdentifier("BIGGR") -> {binary_math_op, kw_biggr};
checkIdentifier("SMALLR") -> {binary_math_op, kw_smallr};
checkIdentifier("BOTH") -> {binary_op, kw_both};
checkIdentifier("EITHER") -> {binary_logic_op, kw_either};
checkIdentifier("WON") -> {binary_logic_op, kw_won};
checkIdentifier("NOT") -> {unary_op, kw_not};
checkIdentifier("ALL") -> {keyword, kw_all};
checkIdentifier("ANY") -> {keyword, kw_any};
checkIdentifier("AN") -> {keyword, kw_an};
checkIdentifier("MKAY") -> {keyword, kw_mkay};
checkIdentifier("SMOOSH") -> {keyword, kw_smoosh};
checkIdentifier("O") -> {keyword, kw_o};
%checkIdentifier("RLY?") -> {keyword, kw_rlyq}; - special handling in getToken
%checkIdentifier("WTF?") -> {keyword, kw_wtfq}; - special handling in getToken
checkIdentifier("OMG") -> {keyword, kw_omg};
checkIdentifier("OMGWTF") -> {keyword, kw_omgwtf};
checkIdentifier("OIC") -> {keyword, kw_oic};
checkIdentifier("YA") -> {keyword, kw_ya};
checkIdentifier("RLY") -> {keyword, kw_rly};
checkIdentifier("NO") -> {keyword, kw_no};
checkIdentifier("WAI") -> {keyword, kw_wai};
checkIdentifier("MEBBE") -> {keyword, kw_mebbe};

checkIdentifier("SAEM") -> {keyword, kw_saem};
checkIdentifier("DIFFRINT") -> {binary_compare_op, kw_diffrint};

checkIdentifier("MAEK") -> {keyword, kw_maek};
checkIdentifier("IS") -> {keyword, kw_is};
checkIdentifier("NOW") -> {keyword, kw_now};

checkIdentifier("WIN") -> {troof, win};
checkIdentifier("FAIL") -> {troof, fail};

checkIdentifier("NOOB") -> {noob, noob};

checkIdentifier("TROOF") -> {type, troof};
checkIdentifier("YARN") -> {type, yarn};
checkIdentifier("NUMBR") -> {type, numbr};
checkIdentifier("NUMBAR") -> {type, numbar};

checkIdentifier("IM") -> {keyword, kw_im};
checkIdentifier("IN") -> {keyword, kw_in};
checkIdentifier("YR") -> {keyword, kw_yr};
checkIdentifier("OUTTA") -> {keyword, kw_outta};

checkIdentifier("VISIBLE") -> {buildin, visible};
checkIdentifier("GIMMEH") -> {buildin, gimmeh};

checkIdentifier("HOW") -> {keyword, kw_how};
checkIdentifier("DUZ") -> {keyword, kw_duz};

checkIdentifier("IF") -> {keyword, kw_if};
checkIdentifier("YOU") -> {keyword, kw_you};
checkIdentifier("SAY") -> {keyword, kw_say};
checkIdentifier("SO") -> {keyword, kw_so};

checkIdentifier("MAH") -> {keyword, kw_mah};

checkIdentifier("GTFO") -> {keyword, kw_gtfo};

checkIdentifier(Str) -> {identifier, Str}.

eatComment([$\n|_]=Cs, Row, Col) -> {Cs, Row, Col};
eatComment([], Row, Col) -> {[], Row, Col};
eatComment([_|Cs], Row, Col) -> eatComment(Cs, Row, Col+1).

eatLongComment([A,$T,$L,$D,$R,B|Cs], Row, Col)
    when (A==$\s orelse A==$\t),(B==$\s orelse B==$\t) -> {[B|Cs], Row, Col+5};
eatLongComment([A,$T,$L,$D,$R,$\n|Cs], Row, _Col)
    when (A==$\s orelse A==$\t) -> {Cs, Row+1, 0};
eatLongComment([$\n|Cs], Row, _Col) -> eatLongComment(Cs, Row+1, 0);
eatLongComment([_|Cs], Row, Col) -> eatLongComment(Cs, Row, Col+1).

eatSpaces([$\s|Cs], Row, Col) -> eatSpaces(Cs, Row, Col+1);
eatSpaces([$\t|Cs], Row, Col) -> eatSpaces(Cs, Row, Col+1);
eatSpaces([$\r|Cs], Row, Col) -> eatSpaces(Cs, Row, Col+1);
eatSpaces([$.,$.,$.,$\n|Cs], Row, _Col) -> eatSpaces(Cs, Row+1, 0);
eatSpaces([$B,$T,$W,A|Cs], Row, Col) when (A==$\s orelse A==$\t orelse A==$\n) ->
	{Rest, NRow, NCol} = eatComment([A|Cs], Row, Col+3),
	eatSpaces(Rest, NRow, NCol);
eatSpaces([$O,$B,$T,$W,A|Cs], Row, Col) when (A==$\s orelse A==$\t orelse A==$\n) ->
	{Rest, NRow, NCol} = eatLongComment([A|Cs], Row, Col+4),
	eatSpaces(Rest, NRow, NCol);
eatSpaces(Cs, Row, Col) -> {Cs, Row, Col}.

getToken(Input, Row, Col) ->
    getToken(undefined, [], Input, Row, Col).

%--- should be eaten by eatSpaces now
%getToken(undefined, [], [$O,$B,$T,$W,$\s|Cs], Row, Col) ->
%	{Rest, NRow, NCol} = eatLongComment(Cs, Row, Col+5),
%	getToken(undefined, [], Rest, NRow, NCol);
%getToken(undefined, [], [$B,$T,$W,$\s|Cs], Row, _Col) -> getToken(undefined, [], eatComment(Cs), Row+1, 0);
%getToken(undefined, [], [$\s|Cs], Row, Col) -> getToken(undefined, [], Cs, Row, Col+1);
%getToken(undefined, [], [$\t|Cs], Row, Col) -> getToken(undefined, [], Cs, Row, Col+1);
%getToken(undefined, [], [$\r|Cs], Row, Col) -> getToken(undefined, [], Cs, Row, Col+1);
%getToken(undefined, [], [$.,$.,$.,$\n|Cs], Row, _) -> getToken(undefined, [], Cs, Row+1, 0);
getToken(undefined, [], [$\n|Cs], Row, _) -> {{separator, Cs}, Row+1, 0};
getToken(undefined, [], [$,|Cs], Row, Col) -> {{separator, Cs}, Row, Col+1};
getToken(undefined, [], [$!|Cs], Row, Col) -> {{exclamation, Cs}, Row, Col+1};
getToken(undefined, [], [$"|Cs], Row, Col) -> getToken(yarn, [], Cs, Row, Col+1);
getToken(undefined, [], [C|Cs], Row, Col) when C >= $0, C =< $9 -> getToken(numbr, [C], Cs, Row, Col+1);
getToken(undefined, [], [C|Cs], Row, Col) when C >= $a, C =< $z -> getToken(identifier, [C], Cs, Row, Col+1);
getToken(undefined, [], [C|Cs], Row, Col) when C >= $A, C =< $Z -> getToken(identifier, [C], Cs, Row, Col+1);

getToken(numbr, TokenStr, [C|Cs], Row, Col) when C >= $0, C =< $9 -> getToken(numbr, TokenStr ++ [C], Cs, Row, Col+1);
getToken(numbr, TokenStr, [C|Cs], Row, Col) when C == $. -> getToken(numbar, TokenStr ++ [C], Cs, Row, Col+1);
getToken(numbr, TokenStr, Input, Row, Col) -> {{{numbr, list_to_integer(TokenStr)}, Input}, Row, Col};

getToken(numbar, TokenStr, [C|Cs], Row, Col) when C >= $0, C =< $9 -> getToken(numbar, TokenStr ++ [C], Cs, Row, Col+1);
getToken(numbar, TokenStr, Input, Row, Col) -> {{{numbar, list_to_float(TokenStr)}, Input}, Row, Col};

getToken(identifier, "RLY", [C|Cs], Row, Col) when C == $? -> {{{keyword, kw_rlyq}, Cs}, Row, Col+4};
getToken(identifier, "WTF", [C|Cs], Row, Col) when C == $? -> {{{keyword, kw_wtfq}, Cs}, Row, Col+4};
getToken(identifier, TokenStr, [C|Cs], Row, Col) when C >= $a, C =< $z -> getToken(identifier, TokenStr ++ [C], Cs, Row, Col+1);
getToken(identifier, TokenStr, [C|Cs], Row, Col) when C >= $A, C =< $Z -> getToken(identifier, TokenStr ++ [C], Cs, Row, Col+1);
getToken(identifier, TokenStr, [C|Cs], Row, Col) when C >= $0, C =< $9 -> getToken(identifier, TokenStr ++ [C], Cs, Row, Col+1);
getToken(identifier, TokenStr, Input, Row, Col) -> {{checkIdentifier(TokenStr), Input}, Row, Col};

getToken(yarn, TokenStr, [$"|Input], Row, Col) -> {{{yarn, TokenStr}, Input}, Row, Col+1};
getToken(yarn, TokenStr, [$:,$"|Cs], Row, Col) -> getToken(yarn, TokenStr ++ [$"], Cs, Row, Col+2);
getToken(yarn, TokenStr, [$:,$:|Cs], Row, Col) -> getToken(yarn, TokenStr ++ [$:], Cs, Row, Col+2);
getToken(yarn, TokenStr, [$:,$)|Cs], Row, Col) -> getToken(yarn, TokenStr ++ [$\n], Cs, Row, Col+2);
getToken(yarn, TokenStr, [$:,$>|Cs], Row, Col) -> getToken(yarn, TokenStr ++ [$\t], Cs, Row, Col+2);
getToken(yarn, TokenStr, [$:,$o|Cs], Row, Col) -> getToken(yarn, TokenStr ++ [$\g], Cs, Row, Col+2);
getToken(yarn, TokenStr, [C|Cs], Row, Col) -> getToken(yarn, TokenStr ++ [C], Cs, Row, Col+1);
getToken(yarn, TokenStr, [], Row, Col) ->  {{error, {yarn, TokenStr, $\0}}, Row, Col};

getToken(TokenType, TokenStr, [], Row, Col) -> {{{TokenType, TokenStr}, []}, Row, Col};
getToken(TokenType, Parsed, [C|_], Row, Col) -> {{error, {TokenType, Parsed, C}}, Row, Col}.

decode(TokenList, Input, NotTrimmedRow, NotTrimmedCol) when is_list(TokenList) ->
	{LTrimmed, Row, Col} = eatSpaces(Input, NotTrimmedRow, NotTrimmedCol),
    case getToken(LTrimmed, Row, Col) of
        {{undefined, []}, []} -> TokenList ;
        {{error, {TokenType, Parsed, C}}, ErrRow, ErrCol} ->
            io:fwrite("Can't create '~p' starting with '~s', at (~B,~B) got '~c'~n", [TokenType, Parsed, ErrRow, ErrCol, C]),
            {error, {TokenType, C}};
        {{Token, []}, _, _} -> TokenList ++ [{Token, {Row, Col}}] ;
        {{Token, Rest}, NewRow, NewCol} -> decode(TokenList ++ [{Token, {Row, Col}}], Rest, NewRow, NewCol)
    end.

scan(Input) ->
    decode([], Input, 0, 0).

%%%%%%%%%%%%%%%%%%

test_set() -> [
    {"value1", fun(A) -> scan(A) end, "abc", [{{identifier, "abc"}, {0, 0}}]},
    {"value2", fun(A) -> scan(A) end, "123", [{{numbr, 123}, {0, 0}}]},
    {"value3", fun(A) -> scan(A) end, "123.45", [{{numbar, 123.45}, {0, 0}}]},
    {"value4", fun(A) -> scan(A) end, "\"a12\"", [{{yarn, "a12"}, {0, 0}}]},
    {"binop1", fun(A) -> scan(A) end, "QUOSHUNT OF 1 AN a", [
		{{binary_math_op,kw_quoshunt}, {0, 0}},
		{{keyword,kw_of}, {0, 9}},
		{{numbr,1}, {0, 12}},
		{{keyword,kw_an}, {0, 14}},
		{{identifier,"a"}, {0, 17}}]},
    {"binop2", fun(A) -> scan(A) end, "BOTH OF WIN AN 1", [
		{{binary_op,kw_both}, {0, 0}},
		{{keyword,kw_of}, {0, 5}},
		{{troof,win}, {0, 8}},
		{{keyword,kw_an}, {0, 12}},
		{{numbr,1}, {0, 15}}]},
    {"binop3", fun(A) -> scan(A) end, "BOTH SAEM WIN AN 1", [
		{{binary_op,kw_both}, {0, 0}},
		{{keyword,kw_saem}, {0, 5}},
		{{troof,win}, {0, 10}},
		{{keyword,kw_an}, {0, 14}},
		{{numbr,1}, {0, 17}}]},
    {"binop4", fun(A) -> scan(A) end, "DIFFRINT WIN AN 1", [
		{{binary_compare_op,kw_diffrint}, {0, 0}},
		{{troof,win}, {0, 9}},
		{{keyword,kw_an}, {0, 13}},
		{{numbr,1}, {0, 16}}]},
    {fun(A) -> scan(A) end, "abc 2b3 7.8 \"anca8:\" c\"", [
		{{identifier,"abc"}, {0, 0}},
		{{numbr,2}, {0, 4}},
		{{identifier,"b3"}, {0, 5}},
		{{numbar,7.8}, {0, 8}},
		{{yarn,"anca8\" c"}, {0, 12}}]},
    {"newline", fun(A) -> scan(A) end, "abc\ndef", [
		{{identifier,"abc"}, {0, 0}},
		{separator, {0, 3}},
		{{identifier,"def"}, {1, 0}}]},
    {"newline cont.", fun(A) -> scan(A) end, "abc...\ndef", [
		{{identifier,"abc"}, {0, 0}},
		{{identifier,"def"}, {1, 0}}]},
    {"endline comment", fun(A) -> scan(A) end, "abc BTW foo\ndef", [
		{{identifier,"abc"}, {0, 0}},
		{separator, {0, 11}},
		{{identifier,"def"}, {1, 0}}]},
    {"multiline comment", fun(A) -> scan(A) end, "abc OBTW foo\nbar TLDR def", [
		{{identifier,"abc"}, {0, 0}},
		{{identifier,"def"}, {1, 9}}]}
    ].

test() -> tester:test(test_set()).
